﻿//Copyright Notice:  ©2009 Microsoft Corporation.  All rights reserved.
using System;
using System.DirectoryServices;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;

namespace Microsoft.InformationSecurity.CISF.Security.AuthZServices
{
    /// <summary>
    /// Utility for getting collections of properties from AD using LDAP.
    /// </summary>
    public static class CISFLDAP
    {
        #region Methods

        #region GetADPropertiesForSingleEntry
        /// <summary>
        /// Gets the AD Property collection for a single directory entry.
        /// </summary>
        /// <param name="lDAPPath">LDAP path.</param>
        /// <param name="searchProperty">Collection of properties to collect from directory entry.</param>
        /// <param name="filterCriteria">AD search criteria.</param>
        /// <returns>
        /// A collection of properties.
        /// </returns>
        /// <remarks>
        /// Telling the search what properties to return doesn't seem to work.  Whether or not properties
        /// are loaded, the search always returns all properties.
        /// </remarks>
        public static PropertyCollection GetADPropertiesForSingleEntry(string lDAPPath, Collection<string> searchProperty, string filterCriteria)
        {
            DirectoryEntry directoryEntry = null;

            //AD query fails when no filter is specified and it would be dangerous to assume one.
            if (String.IsNullOrEmpty(filterCriteria.Trim()))
                throw new ArgumentException("The filter for an Active Directory search must be specified.");

            //Establish required objects to perform search in AD.
            DirectoryEntry searchRoot = new DirectoryEntry(lDAPPath);
            DirectorySearcher directorySearcher = new DirectorySearcher();
            try
            {
                SearchResult searchResult;

                searchRoot.AuthenticationType = AuthenticationTypes.Secure;
                directorySearcher.SearchRoot = searchRoot;
                directorySearcher.Filter = filterCriteria;

                //Get specified properties.  If none were specified, get all properties.
                if (searchProperty != null)
                    if (searchProperty.Count > 0)
                        foreach (string property in searchProperty)
                            directorySearcher.PropertiesToLoad.Add(property);

                try
                {
                    //Get the first directory entry based on the provided filter.
                    searchResult = directorySearcher.FindOne();
                    if (searchResult != null)
                        directoryEntry = searchResult.GetDirectoryEntry();

                }
                catch (DirectoryServicesCOMException)
                {
                    //Ignore the problem and move on.
                    directoryEntry = null;
                }

                if (directoryEntry == null)
                    return null;

                return directoryEntry.Properties;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                directorySearcher.Dispose();
                searchRoot.Close();
            }
        }

        /// <summary>
        /// Gets the AD Property collection for all properties of a single directory entry.
        /// </summary>
        /// <param name="lDAPPath">LDAP path.</param>
        /// <param name="filterCriteria">AD search criteria.</param>
        /// <returns>
        /// A collection of properties.
        /// </returns>
        public static PropertyCollection GetADPropertiesForSingleEntry(string lDAPPath, string filterCriteria)
        {
            return GetADPropertiesForSingleEntry(lDAPPath, null, filterCriteria);
        }

        #endregion

        #region MakesAMAccountTypeQueryString
        /// <summary>
        /// Converts sAMAccount integers into an AD ready query string.
        /// </summary>
        /// <param name="sAMAccountTypes">Collection of sAMAccountTypes to query with.</param>
        /// <returns>
        /// Returns an AD ready query string for sAMAccountTypes.
        /// </returns>
        public static string MakesAMAccountTypeQueryString(Collection<int> sAMAccountTypes)
        {
            string queryFormat = "(sAMAccountType={0})";

            StringBuilder queryBuild = new StringBuilder();
            string multiTypes = "(|{0})";
            string query = String.Format(queryFormat, "*");

            if (sAMAccountTypes.Count > 0)
            {
                foreach (int sAMAccountType in sAMAccountTypes)
                    queryBuild.Append(String.Format(queryFormat, sAMAccountType));

                query = queryBuild.ToString();

                //Add the "Or" to query for multiple account types.
                if (sAMAccountTypes.Count > 1)
                    query = String.Format(multiTypes, query);
            }

            return query;
        }

        #endregion

        #region ParseProperty
        /// <summary>
        /// Gets all values from the property based on the prefix.
        /// </summary>
        /// <param name="property">AD property or path string.</param>
        /// <param name="RDNPrefix">Prefix to parse on that indicates location of values.</param>
        /// <returns>Value represented by key found between comma delimiters.
        /// Returns collection of values based on each instance of the key found in the string.</returns>
        /// <remarks>
        /// This method extracts values starting with the first character after the prefix
        /// and ending with either the last character in the string or the character before 
        /// the next comma found.  Values are trimmed.
        /// 
        /// This method expects will formed AD strings (like distinguishedName).
        /// If there are no commas in the original string, no values are returned.
        /// If a comma is missing between prefixes, erroneous results will occur.
        /// </remarks>
        public static Collection<string> ParseProperty(string property, string RDNPrefix)
        {
            Collection<string> properties = new Collection<string>();
            string[] separator = new string[] { RDNPrefix };
            char[] comma = new char[] { ',' };

            //Split property into multiple strings using comma as delimiter.
            string[] results = property.Split(comma, StringSplitOptions.RemoveEmptyEntries);

            //If no commas were found, then stop processing.
            if (!String.Equals(property, results[0]))
            {
                foreach (string result in results)
                {
                    //Each string should now start with a prefix, so remove the prefix.
                    string[] name = result.Split(separator, StringSplitOptions.RemoveEmptyEntries);

                    //Ignores the result when the prefix isn't found in the string (since Split
                    //returns the whole string when delimiter isn't found.
                    if (!String.Equals(result, name[0]))
                        properties.Add(name[0].Trim());
                }
            }
            else
                //Just return the string.
                properties.Add(results[0].Trim());

            return properties;
        }

        #endregion

        #region IsValidAccount
        /// <summary>
        /// Validates that the OU of the distinguishedName matches the selected sAMAccountTypes.
        /// </summary>
        /// <param name="distinguishedName">The AD distinguishedName to be validated.</param>
        /// <param name="sAMAccountTypes">The sAMAccountTypes to validate against.</param>
        /// <returns></returns>
        public static bool IsValidAccount(string distinguishedName, Collection<int> sAMAccountTypes)
        {
            List<string> validAccounts = new List<string>();

            foreach (uint sAMAccountType in sAMAccountTypes)
            {
                switch (sAMAccountType)
                {
                    case (int)AuthZGlobals.sAMAccountType.SecurityGroup:
                        validAccounts.Add("UserAccounts");
                        break;

                    case (int)AuthZGlobals.sAMAccountType.DistributionList:
                        validAccounts.Add("Distribution Lists");
                        break;

                    default:
                        break;
                }
            }

            Collection<string> types;

            //Get the type of sAMAccount for this distinguisedName.
            if ((types = CISFLDAP.ParseProperty(distinguishedName, AuthZGlobals.RDNPrefix.OrganizationUnitName)).Count > 0)

                //Since distinguishedName string contains only one instance of OU,
                //see if its value is the correct sAMAccountType.
                if (validAccounts.Exists(r => r.Equals(types[0])))
                    return true;

            return false;
        }

        #endregion

        #endregion
    }
}
